/**
 * Copyright 2021 xpstem.com
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.xpstem.lelink.bluetooth;

import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.util.Log;

import com.xpstem.lelink.util.StrUtil;

import java.io.IOException;
import java.io.InputStream;
import java.util.Optional;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

/**
 * @author billy zhang
 */
public class BluetoothConnection {

    public static final String TAG = BluetoothConnection.class.getSimpleName();
    private static final String SPP_UUID = "00001101-0000-1000-8000-00805F9B34FB";

    private ExecutorService executorService = Executors.newCachedThreadPool();

    private Context context;
    private String macAddress;
    private BluetoothManager blueManager;
    private BluetoothSocket socket;
    private Queue<byte[]> messageQueue = new LinkedBlockingQueue<>();
    private WriteRunnable writeRunnable;
    private ReadRunnable readRunnable;
    private boolean open = false;
    private CountDownLatch downLatch;

    private Optional<OnConnectListener> connectListener = Optional.empty();
    private Optional<OnChannelListener> channelListener = Optional.empty();

    public interface OnConnectListener {
        void onConnectSuccess();

        void onConnectFailed();

        void onConnectionLost(Exception ex);
    }

    public interface OnChannelListener {

        void onSend(byte[] data);

        void onReceive(byte[] data);

        void onError(Exception ex);

    }

    public BluetoothConnection(Context context, String macAddress) {
        this.context = context;
        this.macAddress = macAddress;
    }

    public void setOnConnectListener(OnConnectListener listener) {
        connectListener = Optional.ofNullable(listener);
    }

    public void setOnChannelListener(OnChannelListener listener) {
        channelListener = Optional.ofNullable(listener);
    }

    public int connect() {

        blueManager = BluetoothManager.getInstance(context);
        blueManager.setReadVersion(false);

        ConnectRunnable connectRunnable = new ConnectRunnable(macAddress);
        executorService.submit(connectRunnable);

        return 0;
    }

    private void connectSuccess() {

        open = true;

        writeRunnable = new WriteRunnable();
        executorService.submit(writeRunnable);

        readRunnable = new ReadRunnable();
        executorService.submit(readRunnable);

        connectListener.ifPresent(l -> l.onConnectSuccess());
    }

    /**
     * 连接bluetooth线程
     */
    private class ConnectRunnable implements Runnable {
        private String mac;

        public ConnectRunnable(String mac) {
            this.mac = mac;
        }

        @Override
        public void run() {
            try {
                BluetoothDevice remoteDevice = blueManager.getDevice(mac);
                blueManager.stopSearch();

                Log.d(TAG, "prepare to connect: " + remoteDevice.getAddress() + " " + remoteDevice.getName());
                socket = remoteDevice.createInsecureRfcommSocketToServiceRecord(UUID.fromString(SPP_UUID));
                socket.connect();

                connectSuccess();

            } catch (Exception e) {
                e.printStackTrace();
                connectListener.ifPresent(l->l.onConnectFailed());
            }
        }
    }

    private class WriteRunnable implements Runnable {

        @Override
        public void run() {

            try {
                while (open) {
                    byte[] data = messageQueue.poll();

                    if (data != null) {
                        socket.getOutputStream().write(data);
                        socket.getOutputStream().flush();

                        channelListener.ifPresent(l->l.onSend(data));
                    } else {
                        Thread.sleep(100);
                    }
                 }
            } catch (Exception e) {
                connectListener.ifPresent(l->l.onConnectionLost(e));
            } finally {
                if (downLatch != null) {
                    downLatch.countDown();
                }
            }
        }

    }

    private class ReadRunnable implements Runnable {

        @Override
        public void run() {
            try {
                InputStream inputStream = socket.getInputStream();
                while (open) {
                    int n = inputStream.available();
                    if (n > 0) {
                        byte[] buf = new byte[n];
                        inputStream.read(buf);
                        channelListener.ifPresent(l->l.onReceive(buf));
                    } else {
                        Thread.sleep(100);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
                connectListener.ifPresent(l->l.onConnectionLost(e));
            } finally {
                if (downLatch != null) {
                    downLatch.countDown();
                }
            }
        }
    }

    public void send(byte[] data) throws IOException {
        //Log.i(TAG, "send data:" + TypeConversion.bytes2HexString(data));

        messageQueue.add(data);
    }

    public void disconnect() throws IOException {
        open = false;
        downLatch = new CountDownLatch(2);
        try {
            downLatch.await(1, TimeUnit.SECONDS);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        socket.close();
    }

}
